# Copyright (C) 2025 Internet Systems Consortium, Inc. ("ISC")
#
# SPDX-License-Identifier: MPL-2.0
#
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0.  If a copy of the MPL was not distributed with this
# file, you can obtain one at https://mozilla.org/MPL/2.0/.
#
# See the COPYING file distributed with this work for additional
# information regarding copyright ownership.

# Require meson >= 0.64.0 for preserve_path arg in install_data.
# Require meson >= 1.1.0 for meson.options file.
# bug_report is not yet supported by meson, its value will be
# 'kea-dev@lists.isc.org'

project(
    'kea',
    'cpp',
    version: '3.1.5-git',
    meson_version: '>=1.1.0',
    license: 'MPL-2.0',
    license_files: ['COPYING'],
    default_options: [
        'b_asneeded=true',
        'b_lundef=false',  # required for clang++ when used with sanitizers and for BSD ld when linking with extern char **environ.
        'b_pch=false',
        'b_pie=true',
        'install_umask=0027',
        'warning_level=2',
    ],
)

cpp = meson.get_compiler('cpp')
PROJECT_VERSION = meson.project_version()

#### Imports

fs = import('fs')
pkg = import('pkgconfig')
python_module = import('python')

#### Variables

TOP_BUILD_DIR = meson.current_build_dir()
TOP_SOURCE_DIR = meson.current_source_dir()

BINDIR = get_option('bindir')
DATADIR = get_option('datadir')
INCLUDEDIR = get_option('includedir')
LIBDIR = get_option('libdir')
LOCALSTATEDIR = get_option('localstatedir')
MANDIR = get_option('mandir')
PREFIX = get_option('prefix')
RUNSTATEDIR = get_option('runstatedir')
SBINDIR = get_option('sbindir')
SYSCONFDIR = get_option('sysconfdir')

# Meson is annoying with its opinionated alteration of certain paths based on whether prefix is default or not.
# So we revert what it does.
if PREFIX == '/usr/local'
    LOCALSTATEDIR = 'var'    # Otherwise, it would have been 'var/local'.
endif

DATABASE_SCRIPTS_DIR = TOP_BUILD_DIR / 'src/share/database/scripts'
HOOKS_PATH = LIBDIR / 'kea/hooks'
DEFAULT_HOOKS_PATH = PREFIX / HOOKS_PATH
KEA_ADMIN_BUILT = TOP_BUILD_DIR / 'src/bin/admin/kea-admin'
KEA_ADMIN_INSTALLED = PREFIX / SBINDIR / 'kea-admin'
KEA_LFC_BUILT = TOP_BUILD_DIR / 'src/bin/lfc/kea-lfc'
KEA_LFC_INSTALLED = PREFIX / SBINDIR / 'kea-lfc'
LOCALSTATEDIR_INSTALLED = PREFIX / LOCALSTATEDIR
LIBDIR_INSTALLED = PREFIX / LIBDIR
LOGDIR = LOCALSTATEDIR / 'log/kea'
LOGDIR_INSTALLED = PREFIX / LOGDIR
if RUNSTATEDIR == ''
    RUNSTATEDIR = LOCALSTATEDIR / 'run/kea'
else
    RUNSTATEDIR = RUNSTATEDIR / 'kea'
endif
RUNSTATEDIR_INSTALLED = PREFIX / RUNSTATEDIR
SHAREDSTATEDIR = LOCALSTATEDIR / 'lib/kea'
SHAREDSTATEDIR_INSTALLED = PREFIX / SHAREDSTATEDIR
SYSCONFDIR_INSTALLED = PREFIX / SYSCONFDIR
TEST_CA_DIR = TOP_SOURCE_DIR / 'src/lib/asiolink/testutils/ca'

# Meson is annoying with its opinionated alteration of certain paths based on whether prefix is default or not.
# So we revert what it does.
if PREFIX == '/usr/local'
    SYSCONFDIR_INSTALLED = '/usr/local/etc'
    LOGDIR_INSTALLED = '/usr/local/var/log/kea'
    RUNSTATEDIR_INSTALLED = '/usr/local/var/run/kea'
    SHAREDSTATEDIR_INSTALLED = '/usr/local/var/lib/kea'
endif

#### Build Options

crypto_opt = get_option('crypto')
krb5_opt = get_option('krb5')
mysql_opt = get_option('mysql')
netconf_opt = get_option('netconf')
postgresql_opt = get_option('postgresql')

FUZZ_OPT = get_option('fuzz')
TESTS_OPT = get_option('tests')

#### Programs

# External programs used only in this file.
cppcheck = find_program('cppcheck', required: false)
cppcheck_htmlreport = find_program('cppcheck-htmlreport', required: false)
git = find_program('git', required: false)
valgrind = find_program('valgrind', required: false)

AWK = find_program('gawk', 'awk', required: false)
BISON = find_program('bison', version: '>=3.3.0', required: false)
DOXYGEN = find_program('doxygen', required: false)
FLEX = find_program('flex', version: '>=2.6.4', required: false)
INSTALL = find_program('install', required: true)
PDFLATEX = find_program('pdflatex', required: false)
PIP_COMPILE = find_program('pip-compile', required: false)
PLANTUML = find_program('plantuml', required: false)
PYTHON = find_program('python3', 'python', required: true)
SPHINX = find_program('sphinx-build', 'sphinx-build-3', required: false)
SUDO = find_program('sudo', required: false)
XMLLINT = find_program('xmllint', required: false)

CD_AND_RUN = find_program(TOP_SOURCE_DIR / 'scripts/cd-and-run.sh')
ENV = find_program(TOP_SOURCE_DIR / 'scripts/env.sh')
GRABBER = find_program(TOP_SOURCE_DIR / 'scripts/grabber.py')
KEA_MSG_COMPILER = disabler()

#### sudo

PASSWORDLESS_SUDO_SET_UP = false
if SUDO.found()
    result = run_command(SUDO, '-k', check: false)
    if result.returncode() == 0
        result = run_command(SUDO, '-n', 'true', check: false)
        if result.returncode() == 0
            PASSWORDLESS_SUDO_SET_UP = true
        endif
    endif
endif

#### Configuration Data

# TODO: Remaining define macros used in code, but not handled by meson:
# - USE_STATIC_LINK

conf_data = configuration_data(
    {
        'PACKAGE': 'kea',
        'PACKAGE_NAME': 'kea',
        'PACKAGE_VERSION': PROJECT_VERSION,
        'VERSION': f'"@PROJECT_VERSION@"',
    },
)

#### System-specific Variables

SYSTEM = build_machine.system()
if SYSTEM == 'linux'
    conf_data.set('OS_LINUX', true)
    OS_TYPE = 'Linux'
elif SYSTEM == 'freebsd'
    conf_data.set('OS_BSD', true)
    conf_data.set('OS_FREEBSD', true)
    OS_TYPE = 'BSD'
elif SYSTEM == 'netbsd'
    conf_data.set('OS_BSD', true)
    conf_data.set('OS_NETBSD', true)
    OS_TYPE = 'BSD'
elif SYSTEM == 'openbsd'
    conf_data.set('OS_BSD', true)
    conf_data.set('OS_OPENBSD', true)
    OS_TYPE = 'BSD'
elif SYSTEM == 'sun'
    conf_data.set('OS_SOLARIS', true)
    OS_TYPE = 'Solaris'
elif SYSTEM == 'darwin'
    conf_data.set('OS_BSD', true)
    conf_data.set('OS_OSX', true)
    OS_TYPE = 'BSD'
else
    error(f'Unsupported system "@SYSTEM@".')
endif
message(f'Detected system "@SYSTEM@".')

#### Dependencies

boost_dep = dependency('boost', version: '>=1.69', required: false)
if not boost_dep.found()
    boost_dep = dependency('boost', version: '>=1.66', modules: ['system'])
endif
dl_dep = dependency('dl')
threads_dep = dependency('threads')
add_project_dependencies(boost_dep, dl_dep, threads_dep, language: ['cpp'])

# check boost headers
boost_headers = [
    'boost/asio.hpp',
    'boost/asio/coroutine.hpp',
    'boost/asio/io_context.hpp',
    'boost/asio/ip/address.hpp',
    'boost/asio/deadline_timer.hpp',
    'boost/asio/signal_set.hpp',
    'boost/circular_buffer.hpp',
    'boost/date_time/posix_time/posix_time_types.hpp',
    'boost/foreach.hpp',
    'boost/functional/hash.hpp',
    'boost/integer/common_factor.hpp',
    'boost/interprocess/sync/interprocess_upgradable_mutex.hpp',
    'boost/multiprecision/cpp_int.hpp',
    'boost/shared_ptr.hpp',
    'boost/system/error_code.hpp',
]
foreach hdr : boost_headers
    cpp.has_header(hdr, dependencies: [boost_dep], required: true)
endforeach

# Logging
# TODO: remove fallback when support for Ubuntu 20.04 gets removed.
LOG4CPLUS_DEP = dependency('log4cplus', fallback: ['log4cplus', 'log4cplus'])

# Cryptography
CRYPTO_DEP = disabler()
botan = disabler()
foreach dep : ['botan-3', 'botan']
    botan = dependency(dep, version: '>=3.4.0', required: false)
    if botan.found()
        break
    endif
endforeach
openssl = dependency('openssl', required: false)

# Kerberos
KRB5_DEP = dependency(
    'krb5-gssapi',
    fallback: ['krb5', 'krb5'],
    required: krb5_opt,
)
if KRB5_DEP.found()
    cpp.has_header('gssapi/gssapi.h', dependencies: [KRB5_DEP], required: true)
    cpp.has_header(
        'gssapi/gssapi_krb5.h',
        dependencies: [KRB5_DEP],
        required: true,
    )
endif

# MySQL
MYSQL_DEP = dependency(
    'mariadb',
    fallback: ['mysql', 'mysql'],
    required: mysql_opt,
)

# PostgreSQL
POSTGRESQL_DEP = dependency(
    'libpq',
    fallback: ['postgresql', 'postgresql'],
    required: postgresql_opt,
)

# NETCONF
NETCONF_DEP = disabler()
YANG_DEP = disabler()
YANGCPP_DEP = disabler()
SYSREPO_DEP = disabler()
SYSREPOCPP_DEP = disabler()
if netconf_opt.allowed()
    netconf_deps = {}

    all_deps_found = true
    foreach dep : ['yang', 'yang-cpp', 'sysrepo', 'sysrepo-cpp']
        netconf_deps = netconf_deps + {dep: dependency(dep, required: false)}
        if not netconf_deps[dep].found()
            # Try adding lib to it. yang and yang-cpp define the wrong pkg-config.
            netconf_deps = netconf_deps + {
                dep: dependency('lib' + dep, required: false),
            }
        endif

        if not netconf_deps[dep].found()
            all_deps_found = false
            break
        endif
    endforeach

    if all_deps_found
        YANG_DEP = netconf_deps['yang']
        YANGCPP_DEP = netconf_deps['yang-cpp']
        SYSREPO_DEP = netconf_deps['sysrepo']
        SYSREPOCPP_DEP = netconf_deps['sysrepo-cpp']
        NETCONF_DEP = declare_dependency(
            dependencies: [YANG_DEP, YANGCPP_DEP, SYSREPO_DEP, SYSREPOCPP_DEP],
        )
    elif netconf_opt.enabled()
        error('Dependency not found: NETCONF.')
    endif
endif

# Google Test
GTEST_DEP = dependency(
    'gtest',
    fallback: ['gtest', 'gtest_dep'],
    required: FUZZ_OPT.enabled() or TESTS_OPT.enabled(),
)

# Crypto
if crypto_opt == 'botan'
    if botan.found()
        CRYPTO_DEP = botan
    else
        error('botan required but not found')
    endif
elif crypto_opt == 'openssl'
    if openssl.found()
        CRYPTO_DEP = openssl
    else
        error('openssl required but not found')
    endif
endif

if CRYPTO_DEP.name() == botan.name()
    message('Checking Botan Boost support.')
    cpp.has_header('botan/asio_stream.h', dependencies: [botan], required: true)
    conf_data.set('WITH_BOTAN', true)
    message('Using Botan.')
elif CRYPTO_DEP.name() == openssl.name()
    conf_data.set('WITH_OPENSSL', true)
    cpp.has_header(
        'boost/asio/ssl.hpp',
        dependencies: [boost_dep],
        required: true,
    )
    message('Using OpenSSL.')
else
    error('Dependency not found: neither Botan nor OpenSSL.')
endif

# Kea shell
PKGPYTHONDIR = 'unknown'
py_installation = python_module.find_installation('python3', required: false)
if py_installation.found()
    PKGPYTHONDIR = py_installation.get_install_dir(pure: true) / 'kea'
else
    result = run_command(
        PYTHON,
        '-c',
        'import sysconfig; print(sysconfig.get_paths()[\'purelib\'])',
        check: false,
    )
    if result.returncode() == 0
        PKGPYTHONDIR = result.stdout().strip() / 'kea'
    endif
endif

if TESTS_OPT.enabled()
    conf_data.set('ENABLE_DEBUG', true)
    conf_data.set('ENABLE_LOGGER_CHECKS', true)
endif
conf_data.set('FUZZING', FUZZ_OPT.enabled())
conf_data.set('HAVE_MYSQL', MYSQL_DEP.found())
conf_data.set('HAVE_PGSQL', POSTGRESQL_DEP.found())

#### Compiler Checks

# The required keyword in cpp.run() is an 1.5.0 feature.
result = cpp.run(
    fs.read('compiler-checks/get-cpp-standard.cc'),
    name: 'Get cpp standard',
)
if result.returncode() == 0
    cpp_standard = result.stdout().strip()
else
    error('C++ standard is unknown')
endif
message(f'Detected C++ standard (__cplusplus value) is @cpp_standard@.')
cpp_std_opt = get_option('cpp_std')
no_cpp_std_opt_msg = 'Please set a C++ standard by passing the -D cpp_std argument to meson.'
cpp_std_opt_msg = f'-D cpp_std=@cpp_std_opt@ is not enough.'
if cpp_standard.version_compare('<201100')
    msgs = [
        'Kea requires at least C++11 to build.',
        'Recommended C++ standard is C++14 but some dependencies require at least C++20',
    ]
    if cpp_std_opt == 'none'
        msgs += no_cpp_std_opt_msg
    else
        msgs += cpp_std_opt_msg
    endif
    error('\n'.join(msgs))
endif
if cpp_standard.version_compare('<201400')
    result = cpp.run(
        fs.read('compiler-checks/boost-math-cpp14.cc'),
        name: 'BOOST_MATH_REQUIRES_CPP14',
        dependencies: [boost_dep, threads_dep],
    )
    if result.returncode() != 0
        msgs = ['Boost Math requires at least C++14.']
        if cpp_std_opt == 'none'
            msgs += no_cpp_std_opt_msg
        else
            msgs += cpp_std_opt_msg
        endif
        error('\n'.join(msgs))
    endif
endif
if NETCONF_DEP.found() and cpp_standard.version_compare('<202000')
    msgs = ['NETCONF dependency requires at least C++20.']
    if cpp_std_opt == 'none'
        msgs += no_cpp_std_opt_msg
    else
        msgs += cpp_std_opt_msg
    endif
    if netconf_opt.enabled()
        error('\n'.join(msgs))
    else
        msgs += 'Disabling NETCONF.'
        warning('\n'.join(msgs))
        NETCONF_DEP = disabler()
    endif
endif
if CRYPTO_DEP.name() == botan.name() and cpp_standard.version_compare('<202000')
    msgs = ['Botan dependency requires at least C++20.']
    if cpp_std_opt == 'none'
        msgs += no_cpp_std_opt_msg
    else
        msgs += cpp_std_opt_msg
    endif
    error('\n'.join(msgs))
endif

result = cpp.run(
    fs.read('compiler-checks/boost-has-threads.cc'),
    dependencies: [boost_dep, threads_dep],
    name: 'BOOST_HAS_THREADS',
)
if result.returncode() != 0
    error('boost is not configured to use threads')
endif

if cpp.has_header('boost/regex.h', dependencies: [boost_dep], required: false)
    result = cpp.run(
        fs.read('compiler-checks/boost-regex.cc'),
        dependencies: [boost_dep, threads_dep],
        name: 'GET_SYSTEM_VS_BOOST_REGEX_HEADER',
    )
    if result.returncode() != 0
        error('boost/regex.h is used in place of system regex.h')
    endif
endif

result = cpp.run(
    fs.read('compiler-checks/chrono-same-duration.cc'),
    name: 'CHRONO_SAME_DURATION',
)
conf_data.set('CHRONO_SAME_DURATION', result.returncode() == 0)

if CRYPTO_DEP.name() == openssl.name()
    result1 = cpp.run(
        fs.read('compiler-checks/have-generic-tls-method.cc'),
        name: 'HAVE_GENERIC_TLS_METHOD',
        dependencies: [boost_dep, CRYPTO_DEP, threads_dep],
    )
    result2 = cpp.run(
        fs.read('compiler-checks/stream-truncated-error.cc'),
        name: 'HAVE_STREAM_TRUNCATED_ERROR',
        dependencies: [boost_dep, CRYPTO_DEP, threads_dep],
    )
    if result1.returncode() != 0 or result2.returncode() != 0
        error('Boost TLS support broken.')
    endif
endif

if CRYPTO_DEP.name() == botan.name()
    result = cpp.run(
        fs.read('compiler-checks/botan-hash.cc'),
        name: 'CHECK_BOTAN_LIBRARY',
        dependencies: [boost_dep, CRYPTO_DEP, threads_dep],
    )
    if result.returncode() != 0
        error('Botan library does not work.')
    endif
endif

result = cpp.run(
    fs.read('compiler-checks/have-optreset.cc'),
    name: 'HAVE_OPTRESET',
)
conf_data.set('HAVE_OPTRESET', result.returncode() == 0)

result = cpp.run(fs.read('compiler-checks/have-sa-len.cc'), name: 'HAVE_SA_LEN')
conf_data.set('HAVE_SA_LEN', result.returncode() == 0)

result = cpp.run(
    fs.read('compiler-checks/log4cplus-initializer.cc'),
    name: 'LOG4CPLUS_INITIALIZER_H',
    dependencies: [LOG4CPLUS_DEP],
)
conf_data.set('LOG4CPLUS_INITIALIZER_H', result.returncode() == 0)

if MYSQL_DEP.found()
    result = cpp.run(
        fs.read('compiler-checks/mysql-my-bool.cc'),
        name: 'MYSQL_MY_BOOL',
        dependencies: [MYSQL_DEP],
    )
    conf_data.set('HAVE_MYSQL_MY_BOOL', result.returncode() == 0)

    result = cpp.run(
        fs.read('compiler-checks/mysql-get-option.cc'),
        name: 'HAVE_MYSQL_GET_OPTION',
        dependencies: [MYSQL_DEP],
    )
    conf_data.set('HAVE_MYSQL_GET_OPTION', result.returncode() == 0)
endif

result = cpp.run(
    fs.read('compiler-checks/fuzzing-with-clusterfuzzlite.cc'),
    name: 'FUZZING_WITH_CLUSTERFUZZLITE',
)
FUZZING_WITH_CLUSTERFUZZLITE = result.returncode() == 0

have_afl = false
result = cpp.run(fs.read('compiler-checks/have-afl.cc'), name: 'HAVE_AFL')
if result.returncode() == 0
    have_afl = true
endif
conf_data.set('HAVE_AFL', have_afl)

if GTEST_DEP.found()
    # Wrap dependencies cannot be used in compiler checks: https://github.com/mesonbuild/meson/issues/11575
    # We can safely assume that googletest 1.15.2 has CreateUnifiedDiff though.
    if GTEST_DEP.type_name() == 'internal'
        conf_data.set('HAVE_CREATE_UNIFIED_DIFF', true)
    else
        result = cpp.run(
            fs.read('compiler-checks/have-create-unified-diff.cc'),
            name: 'HAVE_CREATE_UNIFIED_DIFF',
            dependencies: [GTEST_DEP],
        )
        conf_data.set('HAVE_CREATE_UNIFIED_DIFF', result.returncode() == 0)
    endif
endif

if KRB5_DEP.found()
    result = cpp.run(
        fs.read('compiler-checks/have-gss-str-to-oid.cc'),
        name: 'HAVE_GSS_STR_TO_OID',
        dependencies: [KRB5_DEP],
    )
    conf_data.set('HAVE_GSS_STR_TO_OID', result.returncode() == 0)
endif

#### Other checks.

if POSTGRESQL_DEP.found()
    version = POSTGRESQL_DEP.version()
    conf_data.set(
        'HAVE_PGSQL_TCP_USER_TIMEOUT',
        version.version_compare('>=12.0'),
    )
endif

# For Solaris.
conf_data.set('HAVE_SYS_FILIO_H', cpp.has_header('sys/filio.h', required: false))

if valgrind.found()
    conf_data.set(
        'HAVE_VALGRIND_HEADERS',
        cpp.has_header('valgrind/valgrind.h', required: false),
    )
endif

result = run_command(cpp, '-dumpmachine', check: false)
if result.returncode() == 0
    d = result.stdout().strip()
    conf_data.set('LIBC_MUSL', d.endswith('-musl'))
endif

if KRB5_DEP.found() and KRB5_DEP.get_variable('vendor').contains('Heimdal')
    conf_data.set('WITH_HEIMDAL', true)
endif

# KEA_PKG_VERSION_IN_CONFIGURE: date and timestamp of the package e.g. "isc20230921141113"
# KEA_PKG_TYPE_IN_CONFIGURE: type of the package "rpm", "deb" or "apk"
kea_pkg_type_in_configure = run_command(
    ENV,
    'KEA_PKG_TYPE_IN_CONFIGURE',
    check: true,
).stdout().strip()
kea_pkg_version_in_configure = run_command(
    ENV,
    'KEA_PKG_VERSION_IN_CONFIGURE',
    check: true,
).stdout().strip()
if kea_pkg_type_in_configure != '' and kea_pkg_version_in_configure != ''
    SOURCE_OF_INSTALLATION = f'@kea_pkg_version_in_configure@ @kea_pkg_type_in_configure@'
elif fs.is_dir('.git')
    SOURCE_OF_INSTALLATION = 'git'
    if git.found()
        result = run_command(
            CD_AND_RUN,
            TOP_SOURCE_DIR,
            git,
            'rev-parse',
            'HEAD',
            check: false,
        )
        if result.returncode() == 0
            SOURCE_OF_INSTALLATION += ' ' + result.stdout().strip()
        endif
    endif
else
    SOURCE_OF_INSTALLATION = 'tarball'
endif
EXTENDED_VERSION = f'@PROJECT_VERSION@ (@SOURCE_OF_INSTALLATION@)'
conf_data.set('EXTENDED_VERSION', f'"@EXTENDED_VERSION@"')
conf_data.set('SOURCE_OF_INSTALLATION', f'"@SOURCE_OF_INSTALLATION@"')

if PROJECT_VERSION.split('.')[1].to_int() % 2 == 0
    package_version_type = 'stable'
else
    package_version_type = 'development'
endif
conf_data.set('PACKAGE_VERSION_TYPE', f'"@package_version_type@"')

#### Compiler

compile_args = []
link_args = []

# TODO: Use $ORIGIN
# $ORIGIN documented at https://www.man7.org/linux/man-pages/man8/ld.so.8.html
# It is preferable since it would allow Kea to start even if the installation is moved.
# We have to be careful at making executables work both in the build directory and in the installation directory.
# Also, Meson might use it by default, but might not use it on all systems, so lots of variables...
# EXECUTABLE_RPATH = f'$ORIGIN/../@LIBDIR@'
# HOOK_RPATH = '$ORIGIN/../..'

BUILD_RPATH = TOP_BUILD_DIR / 'src/lib'
INSTALL_RPATH = LIBDIR_INSTALLED

# Add rpaths for NETCONF dependencies.
if NETCONF_DEP.found()
    # Flag needed to force use of rpath instead of runpath which is transitive
    # e.g. sysrepo is able to find libyang.
    if cpp.has_link_argument('-Wl,--disable-new-dtags')
        add_project_link_arguments(['-Wl,--disable-new-dtags'], language: 'cpp')
    endif

    foreach i : ['yang', 'yang-cpp', 'sysrepo', 'sysrepo-cpp']
        libdir = netconf_deps[i].get_variable('libdir')
        BUILD_RPATH += f':@libdir@'
        INSTALL_RPATH += f':@libdir@'
    endforeach
endif

# rpmbuild complains about rpaths to standard locations so let us conform to
# its liking and remove them. And let's do it consistently for all packages.
if kea_pkg_type_in_configure != ''
    BUILD_RPATH = ''
    INSTALL_RPATH = ''
endif

if SYSTEM == 'darwin'
    compile_args += '-D__APPLE_USE_RFC_3542'
    add_project_arguments('-D__APPLE_USE_RFC_3542', language: 'cpp')
endif

cpp_args_opt = get_option('cpp_args')
werror_opt = get_option('werror')

# List of warnings to add and to remove.
warnings = [
    '-Wnon-virtual-dtor',
    '-Wwrite-strings',
    '-Wno-missing-field-initializers',
    '-Wshadow',
]
no_warnings = ['-Wno-sign-compare']
# Clang++ specific settings.
cxx_id = cpp.get_id()
if cxx_id == 'clang' and cpp_args_opt.length() == 0
    add_project_arguments('-Qunused-arguments', language: 'cpp')
    compile_args += '-Qunused-arguments'
    no_warnings += ['-Wno-unused-variable', '-Wno-unused-parameter']
endif
if cpp.has_argument('-Wdeprecated-literal-operator')
    conf_data.set('WDEPRECATED_LITERAL_OPERATOR_FLAG_SUPPORTED', 'yes')
endif
if werror_opt
    warnings += no_warnings
endif
if cpp_args_opt.length() == 0
    foreach warning : warnings
        if cpp.has_argument(warning)
            add_project_arguments(warning, language: 'cpp')
            compile_args += warning
        else
            message(f'@warning@ is not supported by the compiler')
        endif
    endforeach
endif

#### Premium hooks

premium = fs.is_dir('premium')
if premium
    conf_data.set('PREMIUM', 'yes')
    conf_data.set(
        'PREMIUM_EXTENDED_VERSION',
        f'"yes (@SOURCE_OF_INSTALLATION@)"',
    )
else
    conf_data.set('PREMIUM', 'no')
    conf_data.set('PREMIUM_EXTENDED_VERSION', '"no"')
endif

#### Default Includes

INCLUDES = [
    include_directories('.'),
    include_directories('src'),
    include_directories('src/bin'),
    include_directories('src/lib'),
]

#### Build report

report_conf_data = configuration_data()
report_conf_data.merge_from(conf_data)
report_conf_data.set('TOP_BUILD_DIR', TOP_BUILD_DIR)
report_conf_data.set('PACKAGE_NAME', 'kea')
report_conf_data.set('PACKAGE_VERSION', PROJECT_VERSION)
report_conf_data.set('PACKAGE_VERSION_TYPE', package_version_type)
report_conf_data.set('EXTENDED_VERSION', EXTENDED_VERSION)
report_conf_data.set('OS_TYPE', OS_TYPE)
report_conf_data.set('PREFIX', PREFIX)
report_conf_data.set('HOOKS_DIR', DEFAULT_HOOKS_PATH)
report_conf_data.set('PREMIUM', premium ? 'yes' : 'no')
report_conf_data.set('MESON_VERSION', meson.version())
build_options = meson.build_options()
report_conf_data.set('BUILD_OPTIONS', build_options.replace('\'', '"'))
report_conf_data.set('CXX', ' '.join(cpp.cmd_array()))
report_conf_data.set('CXX_ID', cxx_id)
result = run_command(cpp, '--version', check: false)
if result.returncode() == 0
    v = result.stdout().strip().split('\n')
    report_conf_data.set('CXX_VERSION', v.get(0, 'unknown'))
else
    report_conf_data.set('CXX_VERSION', 'unknown')
endif
report_conf_data.set('CXX_STANDARD', cpp_standard)
compile_args += cpp_args_opt
report_conf_data.set('CXX_ARGS', ' '.join(compile_args))
report_conf_data.set('LD_ID', cpp.get_linker_id())
link_args += get_option('cpp_link_args')
report_conf_data.set('LD_ARGS', ' '.join(link_args))
report_conf_data.set('PYTHON_PATH', PYTHON.full_path())
report_conf_data.set('PYTHON_VERSION', PYTHON.version())
report_conf_data.set('PKGPYTHONDIR', PKGPYTHONDIR)
result = cpp.run(
    fs.read('compiler-checks/get-boost-version.cc'),
    dependencies: [boost_dep, threads_dep],
    name: 'Get Boost version',
)
if result.returncode() == 0
    report_conf_data.set('BOOST_VERSION', result.stdout().strip())
else
    report_conf_data.set('BOOST_VERSION', 'unknown version')
endif
if CRYPTO_DEP.name() == botan.name()
    report_conf_data.set('CRYPTO_NAME', 'Botan')
    report_conf_data.set('SPACES', '                ')
    result = cpp.run(
        fs.read('compiler-checks/get-botan-version.cc'),
        name: 'Get Botan version',
        dependencies: [CRYPTO_DEP],
    )
    if result.returncode() == 0
        version = result.stdout().strip()
    else
        version = botan.version()
    endif
    if version == 'unknown'
        version = 'unknown version'
    endif
    report_conf_data.set('CRYPTO_VERSION', version)
elif CRYPTO_DEP.name() == openssl.name()
    report_conf_data.set('CRYPTO_NAME', 'OpenSSL')
    report_conf_data.set('SPACES', '              ')
    result = cpp.run(
        fs.read('compiler-checks/get-openssl-version.cc'),
        name: 'Get OpenSSL version',
        dependencies: [CRYPTO_DEP],
    )
    if result.returncode() == 0
        version = result.stdout().strip()
    else
        version = openssl.version()
    endif
    if version == 'unknown'
        version = 'unknown version'
    endif
    report_conf_data.set('CRYPTO_VERSION', version)
endif
result = cpp.run(
    fs.read('compiler-checks/get-log4cplus-version.cc'),
    name: 'Get Log4cplus version',
    dependencies: [LOG4CPLUS_DEP],
)
if result.returncode() == 0
    version = result.stdout().strip()
else
    version = log4cplus.version()
endif
if version == 'unknown'
    version = 'unknown version'
endif
report_conf_data.set('LOG4CPLUS_VERSION', version)
if FLEX.found()
    report_conf_data.set('FLEX', FLEX.full_path())
else
    report_conf_data.set('FLEX', 'no')
endif
if BISON.found()
    report_conf_data.set('BISON', BISON.full_path())
else
    report_conf_data.set('BISON', 'no')
endif
if MYSQL_DEP.found()
    report_conf_data.set('HAVE_MYSQL', 'yes')
    version = MYSQL_DEP.version()
    if version == 'unknown'
        version = 'unknown version'
    endif
    report_conf_data.set('MYSQL_VERSION', version)
else
    report_conf_data.set('HAVE_MYSQL', 'no')
    report_conf_data.set('MYSQL_VERSION', 'no')
endif
if POSTGRESQL_DEP.found()
    report_conf_data.set('HAVE_PGSQL', 'yes')
    version = POSTGRESQL_DEP.version()
    if version == 'unknown'
        version = 'unknown version'
    endif
    report_conf_data.set('PGSQL_VERSION', version)
else
    report_conf_data.set('HAVE_PGSQL', 'no')
    report_conf_data.set('PGSQL_VERSION', 'no')
endif
if NETCONF_DEP.found()
    report_conf_data.set('HAVE_NETCONF', 'yes')
    report_conf_data.set('YANG_VERSION', YANG_DEP.version())
    report_conf_data.set(
        'YANG_PREFIX',
        YANG_DEP.get_variable('prefix', default_value: 'unknown'),
    )
    report_conf_data.set('YANGCPP_VERSION', YANGCPP_DEP.version())
    report_conf_data.set(
        'YANGCPP_PREFIX',
        YANGCPP_DEP.get_variable('prefix', default_value: 'unknown'),
    )
    report_conf_data.set('SYSREPO_VERSION', SYSREPO_DEP.version())
    report_conf_data.set(
        'SYSREPO_PREFIX',
        SYSREPO_DEP.get_variable('prefix', default_value: 'unknown'),
    )
    report_conf_data.set('SYSREPOCPP_VERSION', SYSREPOCPP_DEP.version())
    report_conf_data.set(
        'SYSREPOCPP_PREFIX',
        SYSREPOCPP_DEP.get_variable('prefix', default_value: 'unknown'),
    )
else
    report_conf_data.set('HAVE_NETCONF', 'no')
    report_conf_data.set('YANG_VERSION', 'no')
    report_conf_data.set('YANG_PREFIX', 'no')
    report_conf_data.set('YANGCPP_VERSION', 'no')
    report_conf_data.set('YANGCPP_PREFIX', 'no')
    report_conf_data.set('SYSREPO_VERSION', 'no')
    report_conf_data.set('SYSREPO_PREFIX', 'no')
    report_conf_data.set('SYSREPOCPP_VERSION', 'no')
    report_conf_data.set('SYSREPOCPP_PREFIX', 'no')
endif
if FUZZ_OPT.enabled() or TESTS_OPT.enabled()
    report_conf_data.set('HAVE_GTEST', 'yes')
    version = GTEST_DEP.version()
    if version == 'unknown'
        version = 'unknown version'
    endif
    report_conf_data.set('GTEST_VERSION', version)
else
    report_conf_data.set('HAVE_GTEST', 'no')
    report_conf_data.set('GTEST_VERSION', 'no')
endif
if KRB5_DEP.found()
    report_conf_data.set('HAVE_KRB5', 'yes')
    version = KRB5_DEP.version()
    if version == 'unknown'
        version = 'unknown version'
    endif
    report_conf_data.set('KRB5_GSSAPI_VERSION', version)
    report_conf_data.set(
        'KRB5_GSSAPI_VENDOR',
        KRB5_DEP.get_variable('vendor', default_value: 'unknown'),
    )
else
    report_conf_data.set('HAVE_KRB5', 'no')
    report_conf_data.set('KRB5_GSSAPI_VERSION', 'unknown')
    report_conf_data.set('KRB5_GSSAPI_VENDOR', 'unknown')
endif
if TESTS_OPT.enabled()
    report_conf_data.set('TESTS_ENABLED', 'enabled')
else
    report_conf_data.set('TESTS_ENABLED', 'disabled')
endif
if FUZZ_OPT.enabled()
    report_conf_data.set('FUZZ_ENABLED', 'enabled')
else
    report_conf_data.set('FUZZ_ENABLED', 'disabled')
endif
if valgrind.found()
    report_conf_data.set('VALGRIND', valgrind.full_path())
else
    report_conf_data.set('VALGRIND', 'no')
endif
if have_afl
    report_conf_data.set('HAVE_AFL', 'yes')
else
    report_conf_data.set('HAVE_AFL', 'no')
endif

#### Custom Targets

if cppcheck.found()
    run_target(
        'cppcheck',
        command: [
            cppcheck,
            '--check-level=exhaustive',
            '--enable=all',
            '--inline-suppr',
            '--library=googletest',
            '--output-file=cppcheck-result.xml',
            '--project=compile_commands.json',
            '--quiet',
            '--report-progress',
            f'--suppressions-list=@TOP_SOURCE_DIR@/src/cppcheck-suppress.lst',
            '--template={file}:{line}: check_fail: {message} ({severity},{id})',
            '--xml',
            '--xml-version=2',
        ],
    )
endif

if cppcheck_htmlreport.found()
    run_target(
        'cppcheck-report',
        command: [
            cppcheck_htmlreport,
            '--file',
            './cppcheck-result.xml',
            '--report-dir',
            './report',
            '--source-dir',
            TOP_SOURCE_DIR,
            '--title',
            '"cppcheck report"',
        ],
    )
endif

if valgrind.found()
    add_test_setup(
        'valgrind',
        exe_wrapper: [
            valgrind,
            '--child-silent-after-fork=yes',
            '--error-exitcode=0',
            '--fullpath-after=',
            '--leak-check=full',
            '--num-callers=64',
            '--quiet',
            '--show-leak-kinds=all',
            f'--suppressions=@TOP_SOURCE_DIR@/src/valgrind.supp',
            '--xml=yes',
            '--xml-file=valgrind-results-%p.xml',
        ],
        exclude_suites: ['python-tests', 'shell-tests'],
    )
    add_test_setup(
        'valgrind_gen_suppressions',
        exe_wrapper: [
            valgrind,
            '--child-silent-after-fork=yes',
            '--error-exitcode=0',
            '--fullpath-after=',
            '--leak-check=full',
            '--num-callers=64',
            '--quiet',
            '--show-leak-kinds=all',
            f'--suppressions=@TOP_SOURCE_DIR@/src/valgrind.supp',
            '--gen-suppressions=all',
            '--log-file=valgrind.supp',
        ],
        exclude_suites: ['python-tests', 'shell-tests'],
    )
endif

#### Configuration Files

config_report_sh = configure_file(
    input: 'config-report.sh.in',
    output: 'config-report.sh',
    configuration: report_conf_data,
)

CONFIG_REPORT = configure_file(
    output: 'config.report',
    input: config_report_sh,
    command: [TOP_BUILD_DIR / 'config-report.sh'],
)

configure_file(
    input: 'config.h.in',
    output: 'config.h',
    configuration: conf_data,
    install: true,
    install_dir: INCLUDEDIR / 'kea',
)

configure_file(
    input: 'kea_version.h.in',
    output: 'kea_version.h',
    configuration: conf_data,
    install: true,
    install_dir: INCLUDEDIR / 'kea',
)

#### Build Starts Here

# LIBS_BUILT_SO_FAR makes sense because the linker is called with
# the --as-needed flag so ignores (i.e. does not link) unused libraries.
LIBS_BUILT_SO_FAR = []

TARGETS_GEN_MESSAGES = []
TARGETS_GEN_PARSER = []

subdir('tools')
subdir('src')
subdir('fuzz')
subdir('doc')
if premium
    subdir('premium')
endif

#### pkg-config

requires = []
foreach i : [
    boost_dep,
    dl_dep,
    threads_dep,
    CRYPTO_DEP,
    KRB5_DEP,
    LOG4CPLUS_DEP,
    MYSQL_DEP,
    POSTGRESQL_DEP,
    YANG_DEP,
    YANGCPP_DEP,
    SYSREPO_DEP,
    SYSREPOCPP_DEP,
]
    if i.found() and i.type_name() == 'pkgconfig'
        requires += i
    endif
endforeach
pkg.generate(
    description: 'High-performance, extensible DHCP server engine',
    filebase: 'kea',
    libraries: LIBS_BUILT_SO_FAR,
    name: 'Kea',
    requires: requires,
    subdirs: 'kea',
    version: meson.project_version(),
)

#### More Custom Targets

if TARGETS_GEN_MESSAGES.length() > 0
    alias_target('messages', TARGETS_GEN_MESSAGES)
else
    error(
        'No messages to generate. This is probably an error in the meson.build files.',
    )
endif
if TARGETS_GEN_PARSER.length() > 0
    alias_target('parser', TARGETS_GEN_PARSER)
else
    run_target('parser', command: 'echo Parser generation is disabled.'.split())
endif

#### Installation

top_docs = [
    'AUTHORS',
    'CONTRIBUTING.md',
    'COPYING',
    'ChangeLog',
    'README',
    'SECURITY.md',
    'code_of_conduct.md',
    'platforms.rst',
]
install_data(top_docs, install_dir: DATADIR / 'doc/kea')

install_emptydir(LOGDIR)
install_emptydir(RUNSTATEDIR)
install_emptydir(SHAREDSTATEDIR)

# Meson is annoying with its opinionated alteration of certain paths based on whether prefix is default or not.
# So we revert what it does..
# In case prefix is default, install to hardcoded path.
if PREFIX == '/usr/local'
    install_emptydir('/usr/local/lib/kea')
    install_emptydir('/usr/local/var/lib/kea')
    install_emptydir('/usr/local/var/log/kea')
    install_emptydir('/usr/local/var/run/kea')
endif

# Print the setup report.
message(run_command(['cat', CONFIG_REPORT], check: true).stdout())
